<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>数据响应式原理</title>
</head>
<body>
<div id="app">
    Hello Vue
</div>
</body>
<script>
    // 模拟Vue实例中的data选项
    let data={
        msg:'Hello Vue'
    }
    //模拟Vue的实例
    let vm={};

    // //数据劫持，当访问或者设置vm中的成员的时候，做一些干预操作
    // Object.defineProperty(vm,'msg',{
    //     //可枚举(即可被遍历)
    //     enumerable:true,
    //     //可配置(可以使用delete删除，可以通过defineProperty重新定义)
    //     configurable:true,
    //
    //     //当获取值时执行
    //     get(){
    //         console.log('getter:',data.msg);
    //         return data.msg;
    //     },
    //     //当设置、更新msg变量时执行
    //     set(newValue){
    //         console.log("setter:",newValue);
    //         if(data.msg===newValue){
    //             return;//前后数据相同，则不用做操作DOM的多余操作
    //         }
    //         data.msg=newValue;
    //         document.querySelector("#app").textContent=newValue;
    //     }
    // })
    
    function defineProperties(data){
        //循环给每个属性使用Object.defineProperty()
        Object.keys(data).forEach(key => {
            //数据劫持，当访问或者设置vm中的成员的时候，做一些干预操作
            Object.defineProperty(vm,key,{
                //可枚举(即可被遍历)
                enumerable:true,
                //可配置(可以使用delete删除，可以通过defineProperty重新定义)
                configurable:true,

                //当获取值时执行
                get(){
                    console.log('getter:',data[key]);
                    return data[key];
                },
                //当设置、更新msg变量时执行
                set(newValue){
                    console.log("setter:",newValue);
                    if(data[key]===newValue){
                        return;//前后数据相同，则不用做操作DOM的多余操作
                    }
                    data[key]=newValue;
                    document.querySelector("#app").textContent=newValue;
                }
            })
        });
    }

    //执行该函数，使每个属性添加响应式
    defineProperties(data);
    //测试setter
    vm.msg="vue响应式原理";

    //测试getter
    console.log(vm.msg);




    // 公开的Vue类
    class Vue {
        constructor(options) {
            // 保存数据
            this._data = options.data
            // 创建监视者
            observe(this._data)
            // 将数据都映射到实例上
            this._initData()
            // 模板解析
            compile(options.el, this)
        }

        // 遍历数据，映射到实例上
        _initData() {
            for (const key of Object.keys(this._data)) {
                Object.defineProperty(this, key, {
                    get() {
                        return this._data[key]
                    },
                    set(newVal) {
                        this._data[key] = newVal
                    },
                })
            }
        }
    }

    // 创建监视者并返回
    function observe(obj) {
        // 如果不是对象，不需要创建监视者
        if (typeof obj != 'object') return null
        let ob
        if (typeof obj.__ob__ !== 'undefined') {
            ob = obj.__ob__
        } else {
            // 创建监视着，传入对象
            ob = new Observer(obj)
        }
        return ob
    }

    // 监视者
    class Observer {
        constructor(obj) {
            // 创建通知者
            this.dep = new Dep()
            // 将监视者添加到对象的身上
            Object.defineProperty(obj, '__ob__', {
                value: this,
            })
            // 遍历对象数据，定义存取器
            for (let k of Object.keys(obj)) {
                defineReactive(obj, k)
            }
        }
    }

    // 通知者
    class Dep {
        constructor() {
            // 用集合存储自己的订阅者
            this.subs = new Set()
        }
        // 添加订阅者
        addSub(watcher) {
            // 存储订阅者
            this.subs.add(watcher)
        }
        //发布订阅
        notify() {
            // 依次执行更新函数
            // 浅克隆是为了避免在订阅者中修改同一数据，无限更新
            for (const sub of [...this.subs]) {
                sub.update()
            }
        }
        // 标志，表示是否处于依赖收集阶段，值为 Wather
        static target = null
    }

    // 定义存取拦截器，创建闭包环境
    function defineReactive(data, key) {
        // 为数据创建通知者
        const dep = new Dep()
        // 在闭包环境中用局部变量保存数据
        let val = data[key]
        // 子数据如果是对象，也创建监视者
        let childOb = observe(val)

        // 定义存取器
        Object.defineProperty(data, key, {
            // getter
            get() {
                // 如果处于依赖收集阶段
                if (Dep.target != null) {
                    // 添加订阅
                    dep.addSub(Dep.target)
                    // 监视者的通知者也要添加订阅
                    if (childOb != null) {
                        childOb.dep.addSub(Dep.target)
                    }
                }
                // 从局部变量获取值
                return val
            },
            // setter
            set(newValue) {
                if (val === newValue) {
                    return
                }
                // 更新局部变量
                val = newValue
                // 新值也需要尝试创建监视着
                childOb = observe(newValue)
                // 告诉通知者发布订阅
                dep.notify()
            },
        })
    }

    // 订阅者
    class Watcher {
        constructor(vue, expression, callback) {
            this.target = vue
            this.expression = expression
            this.callback = callback
            this.value = this.get()
        }
        update() {
            // 获取新值，如果不相等，则执行更新函数
            const value = this.get()
            if (value !== this.value) {
                this.value = value
                this.callback.call(this.target, value)
            }
        }
        get() {
            // 进入依赖收集阶段，让全局的Dep.target设置成Watcher本身
            Dep.target = this

            // 沿着路径一致寻找
            let val = getObjVal(this.target, this.expression)
            // 依赖收集结束
            Dep.target = null

            return val
        }
    }

    // 根据字符串表达式获取值
    function getObjVal(obj, exp) {
        let val = obj
        exp = exp.split('.')
        exp.forEach((k) => {
            val = val[k]
        })
        return val
    }

    // 编译模板
    function compile(el, vue) {
        // 获取挂载节点
        const $el = document.querySelector(el)

        // 创建片段，存储dom节点
        let fragment = document.createDocumentFragment()

        // 将所有dom节点都放入片段中
        let child
        while ((child = $el.firstChild)) {
            fragment.appendChild(child)
        }

        // 匹配响应式写法
        const reg = /\{\{(.*)\}\}/
        // 遍历子节点
        // 简便起见，直解析文本节点
        for (const node of fragment.childNodes) {
            const text = node.textContent
            if (node.nodeType == 3 && reg.test(text)) {
                // 获取字符串表达式
                let name = text.match(reg)[1].trim()
                // 从vue中获取数据赋值
                node.textContent = getObjVal(vue, name)
                // 创建订阅者，绑定表达式与更新函数
                new Watcher(vue, name, (value) => {
                    node.textContent = value
                })
            }
        }

        // 上树
        $el.appendChild(fragment)
    }
</script>
</html>
